package lowijs.XfireClient;

import static lowijs.XfDroid.XfDroidConstants.EMPTY_STRING;
import static lowijs.XfDroid.XfDroidConstants.PREF_AUTO_STATUS;
import static lowijs.XfDroid.XfDroidConstants.PREF_KEEP_ALIVE;
import static lowijs.XfDroid.XfDroidConstants.PREF_ONLINE_STATUS;
import static lowijs.XfDroid.XfDroidConstants.PREF_ONLINE_STATUS_OLD;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.List;

import lowijs.XfDroid.FriendDBAdapter;
import lowijs.XfDroid.Runner;
import lowijs.XfDroid.XfDroidConstants;
import lowijs.XfDroid.XfDroidService;
import lowijs.XfireClient.event.DatalessEvent;
import lowijs.XfireClient.event.EventManager;
import lowijs.XfireClient.event.XfireEvent;
import lowijs.XfireClient.packet.AckImPacket;
import lowijs.XfireClient.packet.ChangeNickPacket;
import lowijs.XfireClient.packet.ChangeStatusTextPacket;
import lowijs.XfireClient.packet.FriendGamePacket;
import lowijs.XfireClient.packet.FriendStatusPacket;
import lowijs.XfireClient.packet.FriendStatusTextPacket;
import lowijs.XfireClient.packet.FriendslistPacket;
import lowijs.XfireClient.packet.KeepalivePacket;
import lowijs.XfireClient.packet.LoginPacket;
import lowijs.XfireClient.packet.LoginReplyPacket;
import lowijs.XfireClient.packet.NewVersionPacket;
import lowijs.XfireClient.packet.Packet;
import lowijs.XfireClient.packet.ReceiveMessagePacket;
import lowijs.XfireClient.packet.SaltPacket;
import lowijs.XfireClient.packet.ScreenshotsPacket;
import lowijs.XfireClient.packet.VersionPacket;
import lowijs.XfireClient.util.Util;
import lowijs.util.logging.Logging;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager;
import android.util.Log;

import com.googlecode.xfdroid.R;

public class XfireClient implements Runner {
	/**
	 * How often the Pinger class should send keep alive packet.
	 */
	private static final int DEF_KEEPALIVE_EVERY = 60000;

	private DataInputStream in = null;

	private DataOutputStream out = null;

	private String username, password;

	private String statustext;

	private String onlinetext;

	private XfireStatus status;

	private long lastConnection;

	private Pinger pinger;

	private long lastWrite;

	private long when;

	private XfDroidService service;

	private SharedPreferences prefs;

	private int attempt = 0;

	public XfireClient(XfDroidService service) {
		Logging.getLogging().clientCreated(this);

		this.service = service;

		statustext = service.getString(R.string.status_offline);
		setStatus(XfireStatus.DISCONNECTED);
	}

	/**
	 * This class tries to keep the connection alive by periodically re-setting
	 * the status.
	 * @author sgaines
	 */
	class Pinger implements Runnable {
		private boolean running;

		public void run() {
			running = true;
			Logging logging = Logging.getLogging();

			try {
				ActivityManager activity = (ActivityManager)service.getSystemService(Context.ACTIVITY_SERVICE);

				//PackageManager mngr = (PackageManager)service.getPackageManager();
				lastWrite = System.currentTimeMillis();

				{
					List<RunningAppProcessInfo> procs = activity.getRunningAppProcesses();
					logging.clientProcess(procs);
				}

				try {
					List<RunningTaskInfo> tasks = activity.getRunningTasks(100);
					logging.clientTasks(tasks);
				} catch (Exception e) {
				}

				String afk = service.getString(R.string.status_afk);
				PackageManager pm = service.getPackageManager();
				long lastPing = System.currentTimeMillis() - 1000000;
				String keepAliveCode = prefs.getString(PREF_KEEP_ALIVE, Integer.toString(DEF_KEEPALIVE_EVERY));
				int keepAlive = Integer.parseInt(keepAliveCode);

				while (isConnected()) {
					if (service.isScreenOff() || service.isDocked()) {
						changeStatus(afk);
					} else {
						List<RunningAppProcessInfo> procs = activity.getRunningAppProcesses();

						if (prefs.getBoolean(PREF_AUTO_STATUS, false) && procs != null && !procs.isEmpty()) {
							RunningAppProcessInfo curr = null;

							for (RunningAppProcessInfo proc : procs) {
								if (!proc.processName.equals("system") && proc.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
									curr = proc;
								}
							}

							String name = curr.processName;

							if (name.equals("lowijs.XfDroid") || name.equals("system") || name.equals("android.process.acore")) {
								changeStatus(onlinetext);
							} else {
								try {
									ApplicationInfo app = pm.getApplicationInfo(curr.processName, 0);
									if (app != null) {
										CharSequence label = pm.getApplicationLabel(app);

										if (label != null) {
											if (label instanceof String) {
												name = (String)label;
											} else {
												name = label.toString();
											}
										} else if (app.name != null) {
											name = app.name;
										}
									}
								} catch (Exception e) {
								}

								changeStatus(name);
							}
						} else {
							changeStatus(onlinetext);
						}
					}

					String newCode = prefs.getString(PREF_KEEP_ALIVE, keepAliveCode);

					if (!newCode.equals(keepAliveCode)) {
						keepAliveCode = newCode;
						keepAlive = Integer.parseInt(newCode);
					}

					long next = lastPing + keepAlive;
					long now = System.currentTimeMillis();

					if (next <= now) {
						write(KeepalivePacket.KEEPALIVE_PACKET);
						lastPing = now;
						next = lastPing + keepAlive;
					}

					long dif = next - now;

					if (dif > 0) {
						try {
							Thread.sleep(Math.min(dif, 10000));
						} catch (Exception e) {
						}
					}
				}

				logging.pingFinished();
			} catch (Exception e) {
				logging.pingFailed(e);
			} finally {
				running = false;
				logging.pingStopped();
			}
		}

		public void start() {
			if (!running) {
				new Thread(this).start();
			}
		}
	}

	public long getLastConnection() {
		return lastConnection;
	}

	/**
	 * Connect to Xfire using the username and password provided in the constructor.
	 */
	public void connect(String username, String password) {
		this.username = username;
		this.password = password;

		if (status == XfireStatus.CONNECTING || status == XfireStatus.CONNECTED) {
			return;
		}

		setStatus(XfireStatus.CONNECTING);
		service.clientStarting();
	}

	public void disconnect(boolean temporary) {
		Logging.getLogging().clientDisconnect();

		if (close()) {
			service.disconnected(temporary);
		}
	}

	public boolean close() {
		try {
			Logging.getLogging().clientClose();

			if (status == XfireStatus.DISCONNECTED) {
				// If already disconnected, don't process request
				return false;
			}

			setStatus(XfireStatus.DISCONNECTED);

			if (in == null) {
				return true;
			}

			
			InputStream i = in;

			try {
				new Thread(new Runnable() {
					public void run() {
						// sabotage the stream
						Log.d(XfDroidConstants.LOG_TAG, "close(), run(), sabotage stream");
						write(new byte[] { 0, 0, 0, 0 });			
						Log.d(XfDroidConstants.LOG_TAG, "close(), run(), stream has been sabotaged");
					}
				}).start();

				try {
					Log.d(XfDroidConstants.LOG_TAG, "close(), giving sabotage a chance to work (half second)");
					Thread.sleep(500);
				} catch (Exception e) {
				}
			} finally {
				Log.d(XfDroidConstants.LOG_TAG, "close(), closing streams");
				try {in.close();} catch (Exception e) {}
				try {out.close();} catch (Exception e) {}

				synchronized (i) {
					Log.d(XfDroidConstants.LOG_TAG, "close(), nil streams");
					out = null;
					in = null;
				}
			}
		} catch (Exception e) {
			Logging.getLogging().clientException("Problem cloasing", e);
		}

		return true;
	}
	
	public void run() {
		Logging logging = Logging.getLogging();
		logging.clientEnterThread();
		boolean connect = true;
		int event = XfireEvent.XF_OFFLINE;
		int reason = -1;

		SharedPreferences preferences = getPrefs();
		onlinetext = preferences.getString(PREF_ONLINE_STATUS, null);

		if (onlinetext == null) {
			onlinetext = preferences.getString(PREF_ONLINE_STATUS_OLD, null);
		}

		if (onlinetext == null) {
			onlinetext = service.getString(R.string.status_online);
		}

		logging.startConnection();
		while (connect) {
			try {
				logging.connectToXfire();
				Socket s = new Socket("cs.xfire.com", 25999);
				s.setKeepAlive(true);

				//logging.clientGetStreams();
				in = new DataInputStream(s.getInputStream());
				out = new DataOutputStream(s.getOutputStream());
			} catch (Exception e) {
				Logging.getLogging().clientException("Connection to server failed", e);
				event = XfireEvent.XF_CONNECTIONFAIL;
				reason = R.string.connection_failed;
				break;
			}

			logging.performLogin();
			login();

			if (pinger == null) {
				pinger = new Pinger();
			}

			pinger.start();

			try {
				lastConnection = System.currentTimeMillis();

				service.setOnline(true);
				setStatus(XfireStatus.CONNECTED);

				if (handleCommunication()) {
					if (attempt < 10 && waitForReconnect()) {
						attempt++;
						connect = true;
					} else {
						connect = false;
					}
				} else {
					connect = false;
				}
			} catch (Exception e) {
				connect = false;
				logging.clientException("Problem durring communication, closing conneciton", e);
				event = XfireEvent.XF_CONNECTIONFAIL;
				reason = R.string.connection_exception;
			}
		}

		setStatus(XfireStatus.DISCONNECTED);
		stopping(event, reason);
	}
	
	private boolean waitForReconnect() {
		setStatus(XfireStatus.RECONNECTING);

		when = System.currentTimeMillis() + 30000;

		while (status == XfireStatus.RECONNECTING) {
			long time = when - System.currentTimeMillis();

			if (time <= 0) {
				// reconnect
				return true;
			} else {
				try {
					Thread.sleep(time);
				} catch (Exception e) {
				}
			}
		}
		// TODO Auto-generated method stub
		return false;
	}

	private boolean handleCommunication() {
		Logging logging = Logging.getLogging();

		try {
			SharedPreferences preferences = getPrefs();

			while (true) {
				byte[] buffer = readBytes();
				debug(buffer);

				if (buffer == null) {
					// Disconnected!
					return true;
				}

				// This should prevent log out crash
				if (status != XfireStatus.CONNECTED) break;

				int type = ((buffer[0] & 0xFF) | ((buffer[1] & 0xFF) << 8)) & 0xFFFF;
				logging.clientRecievedPacket(type);

				try {
					switch(type) {
					case 0x80: // salt
						SaltPacket sp = new SaltPacket(buffer);
						LoginPacket lp = new LoginPacket(username, password, sp.getSalt());
						write(lp.getBytes());
						break;
					case 0x81: // auth failed
						service.loginFailed();
						logging.clientLoginFailed();
						return false;
					case 0x82: // loginreply
						new LoginReplyPacket(buffer, username);
						changeStatus(EMPTY_STRING);
						changeStatus(onlinetext);
						EventManager.fireEvent(new DatalessEvent(XfireEvent.XF_ONLINE));
						break;
					case 0x83: // friendslist
						new FriendslistPacket(buffer);
						attempt = 0;
						break;
					case 0x84: // friend online
						String[] results = FriendStatusPacket.invoke(buffer);
						service.notify(results);
						break;
					case 0x85: // receive message
						ReceiveMessagePacket rmp = new ReceiveMessagePacket(buffer);
						if (rmp.getMessageType() == ReceiveMessagePacket.MSGTYPE_IM) {
							AckImPacket amp = new AckImPacket(rmp.getSid(), rmp.getImIndex());
							write(amp.getBytes());
						}
						break;
					case 0x86: // new version
						int[] versionArray = NewVersionPacket.getVersions(buffer);
						int version = versionArray[0];
						for (int v : versionArray) {
							version = Math.max(v, version);
						}
						int v = preferences.getInt(VersionPacket.PREF_VERSION, 0);

						//if (v < version) {
							logging.clientNewVersion(version, v);

							SharedPreferences.Editor editor = preferences.edit();
							editor.putInt(VersionPacket.PREF_VERSION, version);
							editor.commit();

							if (reconnect()) {
								//XfireEvent.XF_FLRECV;
							}
						//} else {
						//	logging.clientBadVersion();
						//	disconnect(false);
						//	return false;
						//}

						break;
					case 0x87: // friend in game
						new FriendGamePacket(buffer);
						break;
					case 0x91: // disconnected with reason
						logging.clientBooted();
						disconnect(false);
						return false;
					case 0x9a: // friend status text
						new FriendStatusTextPacket(buffer);
						break;
					case 0xac:
						new ScreenshotsPacket(buffer);
						break;
					case 0x0164:
						Log.d(XfDroidConstants.LOG_TAG, "Buffer Size: " + buffer.length);
						for (String line : Util.toHex(buffer, "\n", 40).split(",")) {
							Log.d(XfDroidConstants.LOG_TAG, line);
						}
						break;
					case 0x0190: // DID - Unknown how to process!
						break;
					default:
						logging.clientUnknownCode(type);
						//disconnect();
						//EventManager.fireEvent(new DatalessEvent(XfireEvent.XF_DCREASON));
						//return;
					}
				} catch (Exception e) {
					logging.clientException("Problem handling packet from server", new PacketException("Problem handling packet", e, buffer));
					stopping(XfireEvent.XF_CONNECTIONFAIL, R.string.packet_exception);
					return true;
				}
			}
		} catch (Exception e) {
			logging.clientException("Error!", e);
		} finally {
			logging.clientFinished();
		}

		return false;
	}

	private void stopping(int event, int id) {
		Logging logging = Logging.getLogging();
		setStatus(XfireStatus.DISCONNECTED);
		service.setOnline(false);
		logging.clientExitThread();
		disconnect(false);
		service.clientClosed(event, id);
	}

	private void login() {
		Logging logging = Logging.getLogging();
		logging.clientLogin();

		// initialize connection with the 'UA01' packet
		write("UA01".getBytes());

		// send the version packet
		VersionPacket vp = new VersionPacket(getPrefs());
		write(vp.getBytes());
	}

	private SharedPreferences getPrefs() {
		if (prefs == null) {
			prefs = PreferenceManager.getDefaultSharedPreferences(service);
		}

		return prefs;
	}

	/**
	 * Sets a new nickname for this Xfire user.
	 * @param nick the nickname to set.
	 */
	public void setNickname(String nickname) {
		ChangeNickPacket cnp = new ChangeNickPacket(nickname);
		write(cnp.getBytes());
		FriendDBAdapter.getInstance(service.getApplicationContext()).nickname(nickname);
	}
	
	public String getStatusText() {
		return statustext;
	}

	private void changeStatus(String statustext) {
		if (statustext == null || statustext.trim().length() == 0) {
			statustext = onlinetext;
		}

		if (!statustext.equals(this.statustext)) {
			this.statustext = statustext;
			ChangeStatusTextPacket cstp = new ChangeStatusTextPacket(statustext);
			write(cstp.getBytes());
		}
	}
	
	public void setStatusText(String statustext) {
		this.onlinetext = statustext;
		changeStatus(statustext);
		Editor editor = getPrefs().edit();
		editor.putString(PREF_ONLINE_STATUS, statustext);
		editor.commit();
	}
	
	public String getUsername() {
		return username;
	}

	public void send(Packet packet) {
		write(packet.getBytes());
	}
	
	/*
	 * I/O and debugging methods
	 */
	private byte[] readBytes() {
		byte[] buffer = null;

		try {
			if (in != null) {
				synchronized (in) {
					int low = in.read();
					int hgh = in.read();
					int len = (0x00 | low | (hgh << 8)) - 2;
			
					if (len <= 0) {
						buffer = new byte[] { 0 };
					} else {
						buffer = new byte[len];
						in.readFully(buffer, 0, len);
					}
				}
			}
		} catch (IOException ioe) {
			if (!ioe.getMessage().equals("The connection was reset")) {
				Logging logging = Logging.getLogging();
				logging.clientException("Durring read", ioe);
			}

			disconnect(true);
			buffer = null;
		}

		return buffer;
	}
	
	public synchronized boolean write(byte[] bs) {
		if (out == null) {
			return false;
		}

		try {
			lastWrite = System.currentTimeMillis();
			out.write(bs);
			return true;
		} catch (IOException ioe) {
			if (
					!ioe.getMessage().equals("The socket argument is not a valid file descriptor") &&
					!ioe.getMessage().equals("The socket argument is not a socket")
			) {
				Logging logging = Logging.getLogging();
				logging.clientException("Durring write", ioe);
			}

			disconnect(true);
			return false;
		}
	}

	private void debug(byte[] bs) {
	}

	public boolean reconnect() {
		close();
		try {Thread.sleep(100);} catch (Exception e) {}

		connect(username, password);

		while (status == XfireStatus.CONNECTING) {
			try {
				Thread.sleep(100);
			} catch (Exception e) {
			}
		}

		if (status == XfireStatus.CONNECTED) {
			Logging.getLogging().clientReconnected(true);
			return true;
		} else {
			Logging.getLogging().clientReconnected(false);
		}

		return false;
	}

	public boolean isConnected() {
		if (status == XfireStatus.DISCONNECTED) {
			Logging.getLogging().clientDisconnected();
			return false;
		}

		return true;
	}

	public int getReconnectTime() {
		if (status == XfireStatus.RECONNECTING) {
			return (int)((when - System.currentTimeMillis()) / 1000);
		} else {
			return -1;
		}
	}

	public void stopReconnect() {
		if (status == XfireStatus.RECONNECTING) {
			setStatus(XfireStatus.DISCONNECTED);
		}
	}

	public boolean isReconnecting() {
		return status == XfireStatus.RECONNECTING;
	}

	private void setStatus(XfireStatus status) {
		this.status = status;
		Logging.getLogging().clientSatusChanged(status);
	}
}
